package main

import (
	"bytes"
	"database/sql"
	"errors"
	"flag"
	"fmt"
	"github.com/PuerkitoBio/goquery"
	"github.com/jmoiron/sqlx"
	"io"
	"log"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
	"vip_video/configx"
	"vip_video/internal/library"
	"vip_video/models"
)

const _BaseUrl = "https://www.nivod.vip"

var configPath = flag.String("config_path", "./config.yaml", "配置文件位置")

var DB *sqlx.DB

var lock = sync.Mutex{}

var CategoryArr = make(map[string]int64)

var videoArr = make(map[string]int64)

// 限制爬取速度
var _limit = make(chan struct{}, 2)

func main() {
	flag.Parse()

	if err := configx.Init(*configPath); err != nil {
		panic(fmt.Sprintf("解析配置失败:%s", err.Error()))
	}

	//初始化数据库
	db, err := library.InitDB(configx.C.Mysql.Dns)
	if err != nil {
		panic(fmt.Sprintf("连接数据库失败:%s", err.Error()))
	}

	DB = db

	for i := 1; i <= 4; i++ {
		if err := _categoryDom(int64(i)); err != nil {
			log.Fatal(err)
		}
	}
}

func _withHeaders() library.RequestLibraryOption {
	return func(requestLibrary *library.RequestLibrary) {
		requestLibrary.Headers = map[string]string{
			//":authority":      "www.nivod.vip",
			//":method":         "GET",
			//":path":           "/label/hot/",
			//":scheme":         "https",
			//"accept":          "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
			//"accept-encoding": "gzip, deflate, br, zstd",
			"accept-language": "zh,und;q=0.9,zh-CN;q=0.8",
			"cache-control":   "no-cache",
			"dnt":             "1",
			"pragma":          "no-cache",
			"priority":        "u=0, i",
			"referer":         "https://www.nivod.vip/t/4/",
			//"sec-ch-ua":                 "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"",
			//"sec-ch-ua-mobile":          "?0",
			//"sec-ch-ua-platform":        "\"macOS\"",
			//"sec-fetch-dest":            "document",
			//"sec-fetch-mode":            "navigate",
			//"sec-fetch-site":            "same-origin",
			//"sec-fetch-user":            "?1",
			"upgrade-insecure-requests": "1",
			"user-agent":                "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
			"cookie":                    "mx_style=black; UM_distinctid=1921d6c95d78ae-0307e311842d7f-17525637-201b88-1921d6c95d8a90; PHPSESSID=ikm60ucj4h6b9qa33p9gjbpkg5; mac_history_mxpro=%5B%7B%22vod_name%22%3A%22%E5%A4%8D%E4%BB%87%E8%80%85%E8%81%94%E7%9B%9F4%EF%BC%9A%E7%BB%88%E5%B1%80%E4%B9%8B%E6%88%98%22%2C%22vod_url%22%3A%22https%3A%2F%2Fwww.nivod.vip%2Fniplay%2F55948-1-3%2F%22%2C%22vod_part%22%3A%221080P%22%7D%5D; CNZZDATA1281382704=1737321263-1727070509-%7C1728099835",
		}
	}
}

// _hotDom 热门爬取
func _hotDom() error {
	var ws sync.WaitGroup
	request := library.NewRequestLibrary(
		library.WithUrl("https://www.nivod.vip/label/hot/"),
		_withHeaders(),
	)
	response, err := request.Get()
	if err != nil {
		return err
	}
	defer response.Body.Close()
	data, err := io.ReadAll(response.Body)
	if err != nil {
		return err
	}
	q, err := goquery.NewDocumentFromReader(bytes.NewReader(data))
	if err != nil {
		return err
	}
	_loadCategoryToMap()
	q.Find(".module-items").Find(".module-card-item").Each(func(i int, selection *goquery.Selection) {
		h, err := selection.Html()
		if err != nil {
			log.Println(err)
			return
		}
		ws.Add(1)
		go func() {
			if err := _hotDomItem(h); err != nil {
				log.Println(err)
			}
			defer ws.Done()
		}()
	})
	ws.Wait()
	return nil
}

// _categoryDom 分类爬取
func _categoryDom(categoryId int64) error {
	var ws sync.WaitGroup
	currentPage := 1
	maxPage := 999
	for {
		err := func() error {
			request := library.NewRequestLibrary(
				library.WithUrl(fmt.Sprintf("https://www.nivod.vip/k/%d--------%d---/", categoryId, currentPage)),
				_withHeaders(),
			)
			response, err := request.Get()
			if err != nil {
				return err
			}
			defer response.Body.Close()
			mainData, err := io.ReadAll(response.Body)
			if err != nil {
				return err
			}
			q, err := goquery.NewDocumentFromReader(bytes.NewReader(mainData))
			if err != nil {
				return err
			}
			//获取一次最大页
			maxUrl, ok := q.Find("#page").Find("a").Last().Attr("href")
			if ok {
				if pMaxPage, err := strconv.Atoi(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(maxUrl, fmt.Sprintf("/k/%d", categoryId), ""), "-", ""), "/", "")); err == nil {
					maxPage = pMaxPage
				}
			}
			q.Find(".module-items").Find("a").Each(func(i int, selection *goquery.Selection) {
				ws.Add(1)
				go func() {
					_limit <- struct{}{}
					defer func() {
						ws.Done()
						<-_limit
					}()
					var c int64 = 9999
					switch categoryId {
					case 1:
						c = 10002
						break
					case 2:
						c = 10003
						break
					case 3:
						c = 10004
						break
					case 4:
						c = 10001
						break
					}
					if err := _categoryDomItem(selection, c); err != nil {
						log.Println(err)
					}
				}()
			})
			return nil
		}()
		if err != nil {
			log.Println(err.Error())
		}
		currentPage++
		if currentPage > maxPage {
			break
		}
	}
	ws.Wait()
	return nil
}

func _categoryDomItem(selection *goquery.Selection, categoryId int64) error {
	//获取链接
	href, ok := selection.Attr("href")
	if !ok {
		return errors.New("未找到详情链接")
	}
	return _videoDetail(fmt.Sprintf("%s%s", _BaseUrl, href), categoryId)
}

// _hotDomItem 热门视频
func _hotDomItem(html string) error {
	q, err := goquery.NewDocumentFromReader(strings.NewReader(html))
	if err != nil {
		return err
	}
	category := q.Find(".module-card-item-class").Text()

	categoryId, err := _getCategoryId(category)
	if err != nil {
		return err
	}
	//获取链接
	href, ok := q.Find(".module-card-item-title").Find("a").Attr("href")
	if !ok {
		return errors.New("未找到详情链接")
	}
	return _videoDetail(fmt.Sprintf("%s%s", _BaseUrl, href), categoryId)
}

// _videoDetail 视频爬取
func _videoDetail(u string, categoryId int64) (err error) {
	request := library.NewRequestLibrary(
		library.WithUrl(u),
		_withHeaders(),
	)
	response, err := request.Get()
	if err != nil {
		return err
	}
	defer response.Body.Close()
	data, err := io.ReadAll(response.Body)
	if err != nil {
		return err
	}
	q, err := goquery.NewDocumentFromReader(bytes.NewReader(data))
	if err != nil {
		return err
	}
	title := q.Find(".module-info-heading").Find("h1").Text()
	imgSrc, ok := q.Find(".module-item-pic").Find("img").Attr("data-original")
	if !ok {
		return errors.New(fmt.Sprintf("%s[%s]:查找图片失败", title, u))
	}
	fileSystem := library.NewSystemFileLibrary(
		library.WithStorageBasePath(os.TempDir()),
	)
	localPath, err := fileSystem.DownloadFile(fmt.Sprintf("%s%s", _BaseUrl, imgSrc))
	if err != nil {
		return err
	}
	imgUrl, err := fileSystem.Upload2System(localPath)
	if err != nil {
		return err
	}
	tx, err := DB.Beginx()
	if err != nil {
		return err
	}
	defer func() {
		if err != nil {
			_ = tx.Rollback()
		}
	}()
	desc := q.Find(".module-info-introduction-content").Text()
	videoId, err := _addVideo(tx, title, imgUrl, desc, categoryId)
	if err != nil {
		return err
	}
	//添加渠道
	channelId, err := _addVideoChannel(tx, videoId)
	if err != nil {
		return err
	}
	//创建章节
	q.Find(".module-play-list-content").Eq(0).Find("a").Each(func(i int, selection *goquery.Selection) {
		if columnHref, ok := selection.Attr("href"); ok {
			//创建章节
			if err := _addChapter(tx, videoId, channelId, selection.Text(), imgUrl, fmt.Sprintf("%s%s", "https://www.nivod.vip", columnHref)); err != nil {
				log.Println("创建章节失败", err)
			}
		}
	})
	_ = tx.Commit()
	fmt.Println("完成爬取:", title)
	//休息 1 秒，避免被反查
	time.Sleep(time.Second)
	return nil
}

// _addChapter 添加章节,不存在不添加
func _addChapter(tx *sqlx.Tx, videoId, channelId int64, title, imgUrl, url string) error {
	//是否存在
	var cnt int64
	if err := tx.Get(&cnt, "select count(*) as cnt from fa_video_chapter where url=?", url); err != nil {
		return err
	}
	if cnt <= 0 {
		_, err := tx.NamedExec(
			"insert into fa_video_chapter(`video_id`,`channel_id`,`title`,`thumb`,`url`,`creator`,`updated_at`) values(:video_id,:channel_id,:title,:thumb,:url,:creator,:updated_at)",
			map[string]any{
				"video_id":   videoId,
				"channel_id": channelId,
				"title":      title,
				"thumb":      imgUrl,
				"url":        url,
				"creator":    "reptile",
				"updated_at": time.Now(),
			},
		)
		if err != nil {
			return err
		}
	}
	return nil
}

// _addVideo 添加或者获取视频id
func _addVideo(tx *sqlx.Tx, title, imgUrl, desc string, categoryId int64) (int64, error) {
	var videoId int64
	//查询视频是否存在
	var videoModel models.VideoModel
	if err := tx.Get(&videoModel, "select * from fa_video where title = ?", title); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			//创建
			videoResult, err := tx.NamedExec(
				"insert into fa_video(`title`,`thumb`,`category_id`,`desc`,`creator`,`updated_at`) values (:title,:thumb,:category_id,:desc,:creator,:updated_at)",
				map[string]any{
					"title":       title,
					"thumb":       imgUrl,
					"category_id": categoryId,
					"desc":        desc,
					"creator":     "reptile",
					"updated_at":  time.Now(),
				},
			)
			if err != nil {
				return 0, err
			}
			videoId, _ = videoResult.LastInsertId()
		} else {
			return 0, err
		}
	} else {
		videoId = videoModel.Id
	}
	return videoId, nil
}

// _addVideoChannel 添加或者获取渠道 id
func _addVideoChannel(tx *sqlx.Tx, videoId int64) (int64, error) {
	var videoChannelId int64
	//是否已经存在
	var videoChannelModel models.VideoChannelModel
	if err := tx.Get(&videoChannelModel, "select * from fa_video_channel where video_id=?", videoId); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			//创建
			res, err := tx.NamedExec("insert into fa_video_channel(video_id,type,handle,creator,`updated_at`) values(:video_id,:type,:handle,:creator,:updated_at)", map[string]any{
				"video_id":   videoId,
				"type":       24,
				"handle":     "nivod",
				"creator":    "reptile",
				"updated_at": time.Now(),
			})
			if err != nil {
				return 0, err
			}
			videoChannelId, _ = res.LastInsertId()
		} else {
			return 0, err
		}
	} else {
		videoChannelId = videoChannelModel.Id
	}
	return videoChannelId, nil
}

// _loadCategoryToMap 加载分类到map里
func _loadCategoryToMap() {
	var list []models.VideoCategoryModel
	if err := DB.Select(&list, "select * from fa_video_category;"); err != nil {
		log.Println("读取影视分类数据失败", err.Error())
	}
	for _, model := range list {
		CategoryArr[model.Title] = model.Id
	}
}

// _getCategoryId 获取分类 id,不存在就创建
func _getCategoryId(value string) (int64, error) {
	lock.Lock()
	defer lock.Unlock()
	value = strings.Trim(value, " ")
	v, ok := CategoryArr[value]
	if ok {
		return v, nil
	}
	var categoryId int64
	//查询是否有这个分类
	var categoryModel models.VideoCategoryModel
	if err := DB.Get(&categoryModel, "select * from fa_video_category where title=?", value); err != nil {
		//创建分类
		result, err := DB.NamedExec("insert into fa_video_category(title,creator,updated_at) values (:title,:creator,:updated_at)", map[string]any{
			"title":      value,
			"creator":    "reptile",
			"updated_at": time.Now(),
		})
		if err != nil {
			return 0, err
		}
		lastId, err := result.LastInsertId()
		if err != nil {
			return 0, err
		}
		categoryId = lastId
	} else {
		categoryId = categoryModel.Id
	}
	//丢进 map 里
	CategoryArr[value] = categoryId
	return categoryId, nil
}

// _loadVideo 记在已存在的电影到map
func _loadVideo() error {
	var list []models.VideoModel
	if err := DB.Select(&list, "select * from fa_video;"); err != nil {
		return err
	}
	for _, model := range list {
		videoArr[model.Title] = model.Id
	}
	return nil
}
